package com.java.zhuiyun.api.uploader.service.impl;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClient;
import com.java.zhuiyun.api.dto.FileChunkDTO;
import com.java.zhuiyun.api.dto.FileChunkResultDTO;
import com.java.zhuiyun.api.file_manager.entity.FileManager;
import com.java.zhuiyun.api.uploader.mapper.UploadMapper;
import com.java.zhuiyun.api.uploader.service.UploadService;
import io.swagger.models.auth.In;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * @ClassName UploadServiceImpl
 * @Description TODO
 * @Author MJL
 * @Date 2023/8/16 13:44
 **/
@Service
public class UploadServiceImpl implements UploadService {

    // 记录日志信息
    private Logger logger = LoggerFactory.getLogger(UploadServiceImpl.class);

    // 配置oss上传图片
    private String endpoint = "oss-cn-beijing.aliyuncs.com";
    private String accessKeyId = "LTAI5tNWqg9ZRvepzeEHvniu";
    private String accessSecret = "28SvhaL2MdwaY97QQowG8C8lrUih76";
    private String bucketName = "it-class-room";

    // Redis缓存
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    UploadMapper uploadMapper;

    @Value("${uploadFolder}")
    private String uploadFolder;

    /**
    * @Author MJL
    * @Description todo 检查文件是否存在，如果存在则跳过该文件的上传，如果不存在，返回需要上传的分片集合
    * @Date 2023/8/16 13:55
    * @Return  * @param null
    */
    // 每天凌晨三点执行清理任务
//    @Scheduled(cron = "0 0 3 * * ?")
    @Override
    public FileChunkResultDTO checkChunkExist(FileChunkDTO chunkDTO) {
        // 获取Redis所有缓存键列表
        Set<String> keys = redisTemplate.keys("*");
        // 遍历缓存键
        for (String key : keys) {
            // 检查缓存中是否存在该键对应的数据
            if (!redisTemplate.hasKey(key)){
                // 如果不存在，则说明缓存已过期，进行清理操作
                redisTemplate.delete(keys);
            }
        }
        System.out.println("缓存数据" +keys);
        //检查文件是否已上传过
        //检查在磁盘中是否存在
        String fileFolderPath = getFileFolderPath(chunkDTO.getIdentifier());
        logger.info("fileFolderPath-->{}", fileFolderPath);
        String filePath = getFilePath(chunkDTO.getIdentifier(), chunkDTO.getFilename());
        File file = new File(filePath);
        boolean exists = file.exists();
        //1.2)检查Redis中是否存在,并且所有分片已经上传完成。
        Set<Integer> uploaded = (Set<Integer>) redisTemplate.opsForHash().get(chunkDTO.getIdentifier(), "uploaded");
        if (uploaded != null && uploaded.size() == chunkDTO.getTotalChunks() && exists) {
            return new FileChunkResultDTO(true);
        }
        File fileFolder = new File(fileFolderPath);
        if (!fileFolder.exists()) {
            boolean mkdirs = fileFolder.mkdirs();
            logger.info("准备工作,创建文件夹,fileFolderPath:{},mkdirs:{}", fileFolderPath, mkdirs);
        }
        // 断点续传，返回已上传的分片
        return new FileChunkResultDTO(false, uploaded);
    }

    /**
    * @Author MJL
    * @Description todo 上传分片
    * @Date 2023/8/16 14:24
    * @Return  * @param null
    */
    @Override
    public void uploadChunk(FileChunkDTO chunkDTO) {
        //分块的目录
        String chunkFileFolderPath = getChunkFileFolderPath(chunkDTO.getIdentifier());
        logger.info("分块的目录 -> {}", chunkFileFolderPath);
        // 获取文件夹
        File chunkFileFolder = new File(chunkFileFolderPath);
        // 判断文件夹是否有文件
        if (!chunkFileFolder.exists()) {
            boolean mkdirs = chunkFileFolder.mkdirs();
            logger.info("创建分片文件夹:{}", mkdirs);
        }
        //写入分片
        try (
                // 创建输入流
                InputStream inputStream = chunkDTO.getFile().getInputStream();
                // 创建输出流
                FileOutputStream outputStream = new FileOutputStream(new File(chunkFileFolderPath + chunkDTO.getChunkNumber()))
        ) {
            // 将输入流的数据复制到输出流
            IOUtils.copy(inputStream, outputStream);
            logger.info("文件标识:{},chunkNumber:{}", chunkDTO.getIdentifier(), chunkDTO.getChunkNumber());
            //将该分片写入redis
            long size = saveToRedis(chunkDTO);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
    * @Author MJL
    * @Description todo 合并分片文件
    * @Date 2023/8/16 14:31
    * @Return  * @param null
    */
    @Override
    public Integer mergeChunk(String identifier, String filename, Integer totalChunks, String uploadedBy) {
        // 获取分块文件的目录路径
        String chunkFileFolderPath = getChunkFileFolderPath(identifier);
        // 获取特定标识符和文件名所对应的文件路径
        String filePath = getFilePath(identifier, filename);
        // 检查分片是否都存在
        if (checkChunks(chunkFileFolderPath, totalChunks)) {
            File chunkFileFolder = new File(chunkFileFolderPath);
            File mergeFile = new File(filePath);
            File[] chunks = chunkFileFolder.listFiles();
            //排序
            List fileList = Arrays.asList(chunks);
            Collections.sort(fileList, (Comparator<File>) (o1, o2) -> {
                return Integer.parseInt(o1.getName()) - (Integer.parseInt(o2.getName()));
            });
            try {
                // 合并分片
                RandomAccessFile randomAccessFileWriter = new RandomAccessFile(mergeFile, "rw");
                byte[] bytes = new byte[1024];
                for (File chunk : chunks) {
                    // 读取分块文件内容
                    RandomAccessFile randomAccessFileReader = new RandomAccessFile(chunk, "r");
                    // 是一个整型变量，用于保存每次读取的字节数。
                    int len;
                    while ((len = randomAccessFileReader.read(bytes)) != -1) {
                        randomAccessFileWriter.write(bytes, 0, len);
                    }

                    /**
                     * 该方法用于关闭读取文件的 RandomAccessFile 对象。在调用该方法之前，不需要执行类似的刷新操作，因为读取操作不会修改文件内容。
                     * 关闭读取的 RandomAccessFile 对象释放了与该文件相关的资源，表示读取文件的操作结束。
                     */
                    randomAccessFileReader.close();
                }
                /**
                 * 该方法用于关闭写入文件的 RandomAccessFile 对象。在调用该方法之前，会执行必要的操作，如将缓冲区中的数据刷新到磁盘上的文件中，以确保数据的持久性存储
                 *关闭写入的 RandomAccessFile 对象表明文件写入操作的结束。
                 */
                randomAccessFileWriter.close();

                // 创建输入流 读取文件中数据
                InputStream inputStream = new FileInputStream(filePath);
                // 创建一个MultipartFile对象，初始化文件名和输入流
                MultipartFile file = new MockMultipartFile(filename, inputStream);
                // 调用这个方法进行文件上传
                String s = this.uploadOssFile(file);
                System.out.println("图片  "+s);
                // 存储文件名称
                File file1 = new File(filename);
                // 获取文件名称
                String name = file1.getName();
                // 截取文件后缀
                String substring = name.substring(name.lastIndexOf("."));
                // 获取文件大小
                long size = file.getSize();

                // 创建存储文件的对象
                FileManager fileManager = new FileManager();
                // 获取加密文件(MD5)
                fileManager.setEncryptFile(identifier);
                // 获取文件名称
                fileManager.setFileManagerName(filename);
                // 获取文件路径
                fileManager.setFilePath(s);
                // 获取分片总数
                fileManager.setShardingTotal(totalChunks);
                // 获取文件大小
                fileManager.setSize(size);
                // 获取文件后缀
                fileManager.setFileType(substring);
                // 获取上传人id
                fileManager.setUploadedBy(Integer.valueOf(uploadedBy));
                // 视频审核状态
                fileManager.setAuditType(0);
                // 创建时间
                fileManager.setCreateTime(new Date());
                // 更新时间
                fileManager.setUpdateTime(new Date());
                // 删除标识
                fileManager.setDelFlag(0);
                // 初始化数据库
                Integer upload = uploadMapper.Upload(fileManager);

                return fileManager.getFileManagerId();
            } catch (Exception e) {
                e.printStackTrace(); // 输出异常信息
                return 0;
            }
//            return true;
        }
        return 0;
    }

    /**
    * @Author MJL
    * @Description todo 上传到oss
    * @Date 2023/8/16 14:35
    * @Return  * @param null
    */
    public String uploadOssFile(MultipartFile file) throws IOException {
        OSS ossClient = new OSSClient(endpoint, accessKeyId, accessSecret);
        // 获取上传文件输入流和文件名称
        InputStream inputStream = file.getInputStream();
        //获取文件名称
        String fileName = file.getName();
        //保证文件名唯一，去掉uuid中的'-'
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        fileName = uuid + fileName;
        //把文件按日期分类，构建日期路径
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd");
        String dateString = dateFormat.format(new Date());
        //拼接
        fileName = dateString + "/" + fileName;
        //上传到阿里云
        ossClient.putObject(bucketName, fileName, inputStream);
        //把上传后把文件url返回
        String url = "https://" + bucketName + "." + endpoint + "/" + fileName;
        //关闭OSSClient
        ossClient.shutdown();
        return url;
    }

    /**
     * @Author MJL
     * @Description 检查分片是否都存在
     * @Date 2023/7/23 18:14
     * @Return boolean
     * @param chunkFileFolderPath
     * @param totalChunks
     */
    private boolean checkChunks(String chunkFileFolderPath, Integer totalChunks) {
        try {
            for (int i = 1; i <= totalChunks + 1; i++) {
                File file = new File(chunkFileFolderPath + File.separator + i);
                if (file.exists()) {
                    continue;
                } else {
                    return false;
                }
            }
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    /**
    * @Author MJL
    * @Description todo 得到分块文件所属的目录
    * @Date 2023/8/16 14:25
    * @Return  * @param null
    */
    private String getChunkFileFolderPath(String identifier) {
        return getFileFolderPath(identifier) + "chunks" + File.separator;
    }

    /**
    * @Author MJL
    * @Description todo 分片写入redis
    * @Date 2023/8/16 14:26
    * @Return  * @param null
    */
    private synchronized long saveToRedis(FileChunkDTO chunkDTO) {
        Set<Integer> uploaded = (Set<Integer>) redisTemplate.opsForHash().get(chunkDTO.getIdentifier(), "uploaded");
        if (uploaded == null) {
            uploaded = new HashSet<>(Arrays.asList(chunkDTO.getChunkNumber()));
            HashMap<String, Object> objectObjectHashMap = new HashMap<>();
            objectObjectHashMap.put("uploaded", uploaded);
            objectObjectHashMap.put("totalChunks", chunkDTO.getTotalChunks());
            objectObjectHashMap.put("totalSize", chunkDTO.getTotalSize());
//            objectObjectHashMap.put("path", getFileRelativelyPath(chunkDTO.getIdentifier(), chunkDTO.getFilename()));
            objectObjectHashMap.put("path", chunkDTO.getFilename());
            redisTemplate.opsForHash().putAll(chunkDTO.getIdentifier(), objectObjectHashMap);
        } else {
            uploaded.add(chunkDTO.getChunkNumber());
            redisTemplate.opsForHash().put(chunkDTO.getIdentifier(), "uploaded", uploaded);
        }
        return uploaded.size();
    }

    /**
    * @Author MJL
    * @Description todo 得到文件的绝对路径
    * @Date 2023/8/16 13:59
    * @Return  * @param null
    */
    private String getFilePath(String identifier, String filename) {
        String ext = filename.substring(filename.lastIndexOf("."));
//        return getFileFolderPath(identifier) + identifier + ext;
        return uploadFolder + filename;
    }
    /**
    * @Author MJL
    * @Description todo 得到文件所属的目录
    * @Date 2023/8/16 13:58
    * @Return  * @param null
    */
    private String getFileFolderPath(String identifier) {
        return uploadFolder + identifier.substring(0, 1) + File.separator +
                identifier.substring(1, 2) + File.separator +
                identifier + File.separator;
//        return uploadFolder;
    }
}
